『Ghosts of Departed Proofs』
2018/7
論文中に登場する「GDP」はこれのことを指す
この論文に用いられているコード
ただし、論文内で型しか示されてないものは、コード内も型しかないmrsekut.icon
大まかな流れ
Abstract
前提条件のある関数を「Ghosts of Departed Proofs (GDP)」を用いて定義する
前提条件を満たしていることを幽霊型を用いて表す
型で表現できるので、実行時オーバーヘッドはない
特に、依存型や線形型などの強力な型システムを使わずに設計する
1 Introduction
目標は、ライブラリ作者として、人間工学的に、安全なAPIを設計すること
GDPの特徴
Properties and proofs are represented in code.
Proofs carried by phantom type parameters.
Library-controlled APIs to create proofs.
Combinators for maipulating ghost proofs.
2 Case Study 1: Sorted Lists
2つのsort済みリストをmergeする関数を、GDPで設計する
ここで、GDPに使ういくつかの便利関数(name,the)や型Namedを導入する
3 Case Study 2: Sharing State Threads
Section 3とSection 4は、あまり望ましくない例として書かれているmrsekut.icon
型安全だが、ちょっと不便
GDPを使ってSTモナドを拡張する例
STモナドで扱うsを2つにして、型でアクセス制限を行う
4 Case Study 3: Key-value Lookups
GDPを使って、Mapをlookupする時に、keyが存在することを型で保証する
5 Case Study 4: Arbitrary Invariants
Section 2, 3, 4で示したものを統括して扱う仕組みを考える
「型に付けた型レベルの名前」と「それに付与する証明」を分離できるようにする
型 ~~ 名前 ::: 証明という型を書く
6 Related Work
7 Summary
Section 2までは嬉しさは理解できる
Sortの例は良いと思うmrsekut.icon
Section 5のheadの定義はどこまで実用的なのか不明mrsekut.icon
読みメモ
Abstract
前提条件のある関数をどう定義するか?
headのように部分関数を定義するか
Maybeなどで失敗しうることを返り値で示すか
「Ghosts of Departed Proofs」という概念についての説明をする
型で済むので実行時のオーバーヘッドがない
1 Introduction
強力な型システムを使わずとも普通のHaskellの型システムで恩恵を受けられる
目標は、ライブラリ作者として、ergonimic(人間工学的)に、安全なAPIを設計すること
ここでの「安全な」とは実行時エラーを起こさないこと
ergonimicとは、利用者に過度の負担をかけないこと
GDPの利用のケーススタディ
APIの間違った使用はコンパイルエラー
いくつかのアプローチ
gdpEndptsのコードの解説はSection 5にあるmrsekut.icon
Ghosts of Departed Proofsの特徴
Properties and proofs are represented in code.
Proofs carried by phantom type parameters.
ここでのcarryはprops drilingみたいな意味合いで言っているmrsekut.icon
関数を連鎖していく時に、その証明済みの型で定義することで、安全な値が複数の関数を通っていくイメージ
幽霊型を使うことで、ランタイムのオーバーヘッドがない
Library-controlled APIs to create proofs.
ライブラリの作者は、そのAPIの動作に関する公理を導入できる唯一の存在者であるべき
具体例を見ないと微妙に何を言っているかわからない #?? 型を公開せずに、関数のみを公開するみたいな話か?
Combinators for maipulating ghost proofs.
2 Case Study 1: Sorted Lists
nameはsmart constructor
2.4でちょっと解説されていたmrsekut.icon
rank2を使うことで、利用者に任意のnameを付けさせずに、nameを与えることができる
コンパイラがexistな名前をつける感じになる
nameは名前を付けた値を直接返すのではなく、「あなたがその名前のついた値で何をしたかったのか言ってください、そうすればあなたのためにそれをします」とユーザーに言う。
どういうこっちゃmrsekut.icon
例
code:hs
module Sorted (Named, SortedBy, sortBy, mergeBy) where
import The
import Named
import Data.Coerce
import qualified Data.List as L
import qualified Data.List.Utils as U
newtype SortedBy comp a = SortedBy a -- compでsort済みであることを表す
instance The (SortedBy comp a) a
sortBy :: ((a -> a -> Ordering) ~~ comp) -- 名前付きcomparator. compoと名付けられたcompartorと読める
-> SortedBy comp a -- このcompでsortされたことを表す sortBy comp xs = coerce (L.sortBy (the comp) xs)
mergeBy :: ((a -> a -> Ordering) ~~ comp)
mergeBy comp xs ys =
coerce (U.mergeBy (the comp) (the xs) (the ys))
mergeBy
3つの引数からnameを取り除いて普通にmergeByしたあとに、
coerceと型宣言によって、SortedBy comp [a]に戻してから返している
ランタイムから見れば、普通のmergeBy'を実行しているのと何も変わらない
sortByが一種のsmart constructorのような役割を担っている
使用する
code:Hs
gdpMain :: IO ()
gdpMain = do
name (comparing Down) $ \gt -> do -- gt::(a->a->Ordering)~name
let xs' = sortBy gt xs
ys' = sortBy gt ys
print (the (mergeBy gt xs' ys'))
nameによって、gtにスコープの決められたnameが付与されている
gtが、名前付きのcompring Downになっている
比較対象として、通常のmergeBy'を使った場合
code:hs
main :: IO ()
main = do
gt = comparing Down
使用感は同じである
mainの方は、ysがsortされていないので、結果は間違ったものになるmrsekut.icon
もちろんコンパイルエラーは出ないのでそれに気付け無い!!!
ソートの方法が間違っている例
code:hs
ng = do
name compare $ \up ->
name (comparing Down) $ \down ->
list2 = sortBy down 1,2,3 in
the (mergeBy up list1 list2) -- type error!
これどうなん?
code:hs
-- comp指定してないから、miniumの結果5になって違うじゃん
miss :: IO ()
miss = do
name (comparing Down) $ \gt -> do
let xs' = sortBy gt xs
print (minimum_O1 xs')
minimum_O1:: SortedBy comp a-> Maybe a minimum_O1 xs = case the xs of
[] -> Nothing
(x:_) -> Just x
いや、これも仕様的には正しいと言えるのか。
Downに対するミニマムだから
何かしらのsortがされている保証を満たしているのでmrsekut.icon
3 Case Study 2: Sharing State Threads
Section 3とSection 4は、あまり望ましくない例として書かれている
GDPを使ってSTモナドを拡張する
この論文内では、曖昧さ回避のため(STではなく)Stとする
2つの共有メモリ(s,s')を操作するAPIを設計している
Left側では両方のメモリ内を操作できるが、Rightでは片方しか操作できないように型で制限している
code:hs
data Region = Region
type St s a = State (Region ~~ s) a
runSt :: (forall s. St s a) -> a
runSt action = name Region (evalState action)
newRef :: a -> St s (STRef s a)
newRef = undefined
readRef :: STRef s a -> St s a
readRef = undefined
writeRef :: STRef s a -> a -> St s ()
writeRef = undefined
runSt2 :: (forall s s'. St (s, s') a) -> a
runSt2 action = undefined
liftL :: St s a -> St (s,s') a
liftL = undefined
liftR :: St s' a -> St (s,s') a
liftR = undefined
share :: STRef s a -> St s (STRef (s,s') a)
share = undefined
use :: STRef (s,s') a -> STRef s a
use = undefined
symm :: STRef (s,s') a -> STRef (s',s) a
symm = undefined
stSharing :: Bool
stSharing = runSt2 $ do
(secret, ref) <- liftL $ do
unshared <- newRef 42
shared <- share =<< newRef 17
return (unshared, shared)
liftR $ do
let mine = use (symm ref)
x <- readRef mine
writeRef mine (x + 1)
liftL $ do
check <- readRef secret
return (check == 42)
4 Case Study 3: Key-value Lookups
Section 3とSection 4は、あまり望ましくない例として書かれている
Mapをlookupする時に、keyが存在することを型で保証したい
code:hs
-- type Diagraph v = Map v v code:hs
newtype JMap ks k v = JMap (Map k v) deriving Functor
newtype Key ks k = Key k
instance The (JMap ks k v) (Map k v)
instance The (Key ks k) k
member :: k -> JMap ks k v -> Maybe (Key ks k)
member = undefined
lookup:: Key ks k -> JMap ks k v -> v
lookup = undefined
reinsert:: Key ks k -> v -> JMap ks k v -> JMap ks k v
reinsert = undefined
withMap:: Map k v -> (forall ks. JMap ks k v -> t) -> t
withMap _ _ = undefined
ksはkey set
Key ks k
ksというkey setにkが存在することを示す型
JMap ks k v
ksというkey setを持つMap k v
withMap
nameと似た役割
ksをMapにアタッチする
member
keyがksに存在するかどうか判定
lookup
安全なlookup
特定のkは、一つのMapの中にのみ属すことも保証する
5 Case Study 4: Arbitrary Invariants
これまで2種類の方法を示した
name自体をを使うもの
runST2やwithMapのようなlibraryが用意したrank 2の関数を使うもの
Section 3とSection 4は、あまり望ましくない例として書かれているmrsekut.icon
「型に付けた型レベルの名前」と「それに付与する証明」を分離できるようにする
a ~~ n ::: pと表記する
aは、元々の型
nという型レベルの名前を持つ
pは証明で、この条件を満たすことを表すことを表現する
これを使うことで、任意の条件を幽霊型として符号化できる
code:hs
gdpHead :: (a ~~ xs ::: IsCons xs) -> a gdpHead xs = head (the xs)
引数[a]は、IsCons xsを満たすものである
xsという名前が付いているため、これを型の定義内で使い回せる
(上記のコードでは使いまわしてはいない)
更にこの例だと、元のPrelude.headの定義をそのまま使い回せる
NonEmpty aと[a]で同じ関数の再定義が不要になっている
でもこれ、結局パターンマッチ出てるし、そんなに嬉しいか?
ここにProof pという証明を用意すると、
a ::: pで、aに証明を適用できる
エンドユーザーは知る必要はない
普通に使える
Defnが出てきた辺りからよくわからんくなってきたなmrsekut.icon
この辺から、応用すぎて何を言っているのか理解できないmrsekut.icon
6 Related Work
7 Summary
headに関してGhosts of Departed Proofsを使う嬉しさは何なのか